# -*- coding: utf-8 -*-
"""
@Author: Logan
@Date:   2025/6/29 16:21
@Description:
封装了 Selenium 自动化操作的核心工具方法，用于安全定位元素、处理页面加载、完成点击 / 输入等操作，解决动态网页中元素定位不稳定的问题。
1. element_get
简述：获取元素，可选择是否需要元素可见。默认只要元素存在就返回，也可强制等待元素可见后返回。
2. wait_for_ready_state_complete
简述：等待网页完全加载（如 HTML、图片等资源加载完成），确保页面处于可操作状态。
3. element_disappear
简述：等待指定元素消失（不可见或不存在），常用于等待加载动画、弹窗等消失后再继续操作。
4. element_appear
简述：等待指定元素出现并可见，确保元素能被用户看到后再进行点击、输入等操作。
5. element_is_display
简述：判断元素是否存在（存在返回 True，不存在返回 False），快速检查元素状态。
6. element_to_url
简述：跳转到指定网页，同时等待页面加载完成、旧元素消失和新元素出现，确保跳转成功。
7. element_fill_value
简述：向元素（如输入框）填充内容，自动清空原有内容，支持输入后回车，处理输入过程中的异常。
8. element_click
简述：点击指定元素，确保元素可见且可点击，点击后可等待新元素出现或旧元素消失，验证点击效果。
"""
import datetime
import os
import time
from selenium.common.exceptions import ElementNotVisibleException, WebDriverException, NoSuchElementException, \
    StaleElementReferenceException
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains

from common.find_image import FindImg
from common.report_add_img import add_img_path_2_report
from common.tools import get_project_path, sep
from config.yaml_config import GetConf
from common import find_image


class ObjectMap:

    def __init__(self):
        self.url = GetConf().get_url()

    @staticmethod
    def element_get(driver, locate_type, locator_expression, timeout=15, must_be_visible=False):
        """
        单个元素获取，元素可以不可见
        :param driver: 浏览器驱动
        :param locate_type: 定位方式类型
        :param locator_expression: 定位表达式
        :param timeout:超时时间
        :param must_be_visible:元素是否必须可见，True为必须可见，默认为false
        :return: 返回获取到的元素
        """
        # 开始时间
        start_ms = time.time() * 1000.0
        # 设置的结束时间
        stop_ms = start_ms + (timeout * 1000.0)
        for x in range(int(timeout * 10)):
            try:
                # 获取元素
                element = driver.find_element(by=locate_type, value=locator_expression)
                # 如果元素不是必须可见的，就直接返回元素
                if not must_be_visible:
                    return element
                # 如果元素必须是可见的，则需要先判断元素是否可见
                else:
                    if element.is_displayed():
                        return element
                    else:
                        raise Exception()
            except Exception:
                now_ms = time.time() * 1000.0
                if now_ms >= stop_ms:
                    break
                time.sleep(0.1)
        raise ElementNotVisibleException("元素定位失败,定位方式:" + locate_type + " 定位表达式:" + locator_expression)

    @staticmethod
    def wait_for_ready_state_complete(driver, timeout=30):
        """
        等待页面完全加载完成
        :param driver:
        :param timeout:
        :return:
        """
        # 开始时间
        start_ms = time.time() * 1000.0
        # 设置的结束时间
        stop_ms = start_ms + (timeout * 1000.0)

        for x in range(int(timeout * 10)):
            try:
                # 获取页面的一个 "是否准备完成" 的属性
                ready_state = driver.execute_script("return document.readyState")
            except WebDriverException:
                # 如果有driver错误，执行js会失败，就直接跳过
                time.sleep(0.03)
                return True
            # 如果页面元素全部加载完，返回True
            if ready_state == "complete":
                time.sleep(0.01)
                return True
            else:
                now_ms = time.time() * 1000.0
                # 如果超时了就break
                if now_ms >= stop_ms:
                    break
                time.sleep(0.1)
        raise Exception("打开网页时，页面元素在%s秒后仍然没有完全加载完" % timeout)

    @staticmethod
    def element_disappear(driver, locate_type, locator_expression, timeout=30):
        """
        等待页面元素消失
        :param driver: 浏览器驱动
        :param locate_type: 定位方式类型
        :param locator_expression: 定位表达式
        :param timeout: 超时时间
        :return:
        """
        if locate_type:
            # 开始时间
            start_ms = time.time() * 1000.0
            # 设置的结束时间
            stop_ms = start_ms + (timeout * 1000.0)
            for x in range(int(timeout * 10)):
                try:
                    element = driver.find_element(by=locate_type, value=locator_expression)
                    if element.is_displayed():
                        now_ms = time.time() * 1000.0
                        if now_ms >= stop_ms:
                            break
                        time.sleep(0.1)
                # 定位不到元素，表示元素已经消失，返回true
                except Exception:
                    return True
            raise Exception("元素没有消失,定位方式:' + locate_type + '\n定位表达式:' + locator_expression")
        else:
            pass

    @staticmethod
    def element_appear(driver, locate_type, locator_expression, timeout=30):
        """
        等待页面元素出现
        :param driver: 浏览器驱动
        :param locate_type: 定位方式类型
        :param locator_expression: 定位表达式
        :param timeout: 超时时间
        :return: 元素出现返回True
        """
        if locate_type:
            # 开始时间
            start_ms = time.time() * 1000.0
            # 设置的结束时间
            stop_ms = start_ms + (timeout * 1000.0)
            for x in range(int(timeout * 10)):
                try:
                    # 定位元素
                    element = driver.find_element(by=locate_type, value=locator_expression)
                    # 元素是否可见
                    if element.is_displayed():
                        return element
                    else:
                        raise Exception()
                # 元素没出现或者元素不可见，判断超时
                except Exception:
                    now_ms = time.time() * 1000.0
                    if now_ms >= stop_ms:
                        break
                    time.sleep(0.1)
            raise ElementNotVisibleException(
                "元素没有出现,定位方式:" + locate_type + " 定位表达式:" + locator_expression)
        else:
            pass

    @staticmethod
    def element_is_display(driver, locate_type, locator_expression):
        """
        元素是否显示
        :param driver: 浏览器驱动
        :param locate_type: 定位方式类型
        :param locator_expression: 定位表达式
        :return:
        """
        try:
            driver.find_element(by=locate_type, value=locator_expression)
            return True
        except NoSuchElementException:
            # 发生了NoSuchElementException异常，说明页面中未找到该元素，返回False
            return False

    def element_to_url(self, driver, url, locate_type_disappear=None, locator_expression_disappear=None,
                       locate_type_appear=None, locator_expression_appear=None, ):
        """
        跳转地址
        :param driver:驱动对象
        :param url:跳转地址
        :param locate_type_disappear:等待元素消失的定位方式
        :param locator_expression_disappear:等待元素消失表达式
        :param locate_type_appear:等待元素出现的定位方式
        :param locator_expression_appear:等待元素出现表达式
        :return:
        """
        try:
            driver.get(self.url + url)
            # 等待页面元素都加载完
            self.wait_for_ready_state_complete(driver)
            # 跳转地址后等待元素消失
            self.element_disappear(driver, locate_type_disappear, locator_expression_disappear)
            # 跳转地址后等待元素出现
            self.element_appear(driver, locate_type_appear, locator_expression_appear)
        except Exception as e:
            print("跳转地址出现异常，异常原因:%s" % e)
            return False
        return True

    def element_fill_value(self, driver, locate_type, locator_expression, fill_value, timeout=30):
        """
        元素填值（先清除元素中的值，再填值，如果填入的值以'\n'结尾，则自动回车，输入值后检测输入的值是否与入参一致）
        :param driver: 浏览器驱动
        :param locate_type: 定位方式类型
        :param locator_expression: 定位表达式
        :param fill_value: 填入的值
        :param timeout:超时时间
        :return:
        """
        # 元素必须先出现，出现返回该元素
        element = self.element_appear(driver=driver,
                                      locate_type=locate_type,
                                      locator_expression=locator_expression,
                                      timeout=timeout)
        # self.scroll_to_element(driver, locate_type, locator_expression)
        try:
            # 先清除元素中原有的值
            element.clear()
        except StaleElementReferenceException:  # 页面元素未刷新出来，就对元素进行捕获，因此引发该异常。
            self.wait_for_ready_state_complete(driver=driver)
            time.sleep(0.06)
            element = self.element_appear(
                driver=driver,
                locate_type=locate_type,
                locator_expression=locator_expression,
                timeout=timeout,
            )
            try:
                element.clear()
            except Exception:
                pass
        except Exception:
            pass
        # 填入的值都转成字符串
        if type(fill_value) is int or type(fill_value) is float:
            fill_value = str(fill_value)
        try:
            if not fill_value.endswith("\n"):
                element.send_keys(fill_value)
                self.wait_for_ready_state_complete(driver=driver)
            else:
                # 如果填入的值后面跟了'\n'，就加一步回车的操作
                fill_value = fill_value[:-1]
                element.send_keys(fill_value)
                element.send_keys(Keys.RETURN)
                self.wait_for_ready_state_complete(driver=driver)
        except StaleElementReferenceException:
            self.wait_for_ready_state_complete(driver=driver)
            time.sleep(0.06)
            element = self.element_appear(
                driver=driver,
                locate_type=locate_type,
                locator_expression=locator_expression
            )
            element.clear()
            if not fill_value.endswith("\n"):
                element.send_keys(fill_value)
            else:
                fill_value = fill_value[:-1]
                element.send_keys(fill_value)
                element.send_keys(Keys.RETURN)
                self.wait_for_ready_state_complete(driver=driver)
        except Exception:
            raise Exception("元素填值失败")

        return True

    def element_click(self, driver, locate_type, locator_expression, wait_for_locate_type=None,
                      wait_for_locator_expression=None, wait_for_disappear_locate_type=None,
                      wait_for_disappear_locator_expression=None, timeout=30):
        """
        元素点击
        :param driver: 浏览器驱动
        :param locate_type: 定位方式类型
        :param locator_expression: 定位表达式
        :param wait_for_locate_type: 等待元素出现的元素定位方式类型
        :param wait_for_locator_expression: 等待元素出现的元素定位表达式
        :param wait_for_disappear_locate_type:等待元素消失的元素定位方式类型
        :param wait_for_disappear_locator_expression:等待元素消失的元素定位表达式
        :param timeout:
        :return:
        """
        # 元素要可见
        element = self.element_appear(
            driver=driver,
            locate_type=locate_type,
            locator_expression=locator_expression,
            timeout=timeout,
        )
        try:
            # 点击元素
            element.click()
        except StaleElementReferenceException:
            self.wait_for_ready_state_complete(driver=driver)
            time.sleep(0.05)
            element = self.element_appear(
                driver=driver,
                locate_type=locate_type,
                locator_expression=locator_expression,
                timeout=timeout,
            )
            element.click()
        except Exception as e:
            print("页面出现异常，元素不可点击", e)
            return False

        try:
            # 点击元素后的新元素出现或旧元素消失
            self.element_appear(
                driver, wait_for_locate_type, wait_for_locator_expression
            )
            self.element_disappear(
                driver,
                wait_for_disappear_locate_type,
                wait_for_disappear_locator_expression,
            )
        except Exception as e:
            print("等待元素消失或出现失败", e)
            return False
        return True

    def upload(self, driver, locate_type, locator_expression, file_path):
        """
        文件上传
        :param driver:
        :param locate_type:
        :param locator_expression:
        :param file_path:
        :return:
        """
        print(file_path)
        element = self.element_get(driver, locate_type, locator_expression)
        element.send_keys(file_path)

    def switch_window_2_latest_handle(self, driver):
        """
        句柄切换窗口到最新的窗口
        :param driver:
        :return:
        """
        window_handles = driver.window_handles
        driver.switch_to.window(window_handles[-1])

    def switch_into_iframe(self, driver, locate_iframe_type, locate_iframe_expression):
        """
        进入iframe(内嵌网页)
        :param driver:浏览器驱动
        :param locate_iframe_type:定位iframe的方式
        :param locate_iframe_expression:定位iframe的表达式
        :return:
        """
        iframe = self.element_get(driver, locate_iframe_type, locate_iframe_expression)
        driver.switch_to.frame(iframe)

    @staticmethod
    def switch_from_iframe_to_content(driver):
        """
        从iframe切回主文档
        :param driver:
        :return:
        """
        driver.switch_to.parent_frame()

    def hover_and_click(self, driver, locate_hover_type, locate_hover_expression, locate_click_type,
                        locate_click_expression, timeout=30):
        """
        悬停在指定元素上，并点击出现的下拉菜单选项
        :param driver:WebDriver实例
        :param locate_hover_type:悬停元素的定位方式
        :param locate_hover_expression:悬停元素的定位表达式
        :param locate_click_type:点击元素的定位方式
        :param locate_click_expression:点击元素的定位表达式
        :param timeout:
        """
        try:
            # 获取元素
            hover_element = self.element_get(driver, locate_hover_type, locate_hover_expression)
            click_element = self.element_get(driver, locate_click_type, locate_click_expression)

            # 创建连贯的操作链
            actions = ActionChains(driver)
            actions.move_to_element(hover_element)  # 悬停

            # 模拟等待（注意：ActionChains中不能直接使用sleep，需用move_by_offset）
            # 这里通过微小移动保持悬停状态
            actions.move_by_offset(1, 1).move_by_offset(-1, -1)

            # 移动到目标元素并点击
            actions.move_to_element(click_element).click()

            # 一次性执行所有操作
            actions.perform()
            return True
        except Exception as e:
            print(f"操作失败: {e}")
            return False

    def find_img_in_source(self, driver, img_name):
        """
        截图并在截图中查找图片
        :param driver:
        :param img_name:
        :return:
        """
        # 截图后图片存放的路径
        source_img_path = get_project_path() + sep(["common", "img", "source_img", "source-" + img_name],
                                                   add_sep_before=True)
        # 截图并保存图片
        driver.get_screenshot_as_file(source_img_path)
        time.sleep(5)
        if not os.path.exists(source_img_path):
            raise FileNotFoundError(f"截图保存失败: {source_img_path}")
        # 需要查找的图片的路径
        search_img_path = get_project_path() + sep(["common", "img", "assert_img", img_name], add_sep_before=True)
        # 将原图和查找的图片添加到测试报告中
        add_img_path_2_report(source_img_path, "原图")
        add_img_path_2_report(search_img_path, "需要查找的图片")
        # 在原图中查找是否有指定的图片，返回信心值
        confidence = FindImg().get_confidence(source_img_path, search_img_path)
        print(confidence)
        return confidence

    def element_screenshot(self, driver, locate_type, locator_expression):
        """
        元素截图
        :param driver:
        :param locate_type:
        :param locator_expression:
        :return:元素图片的路径
        """
        ele_name = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + ".png"  # 获取当前时间给元素命名
        ele_img_dir_path = get_project_path() + sep(["common", "img", "ele_img"], add_sep_before=True,  # 元素截图存放目录
                                                    add_sep_after=True)
        # 如果不存在该目录，则创建
        if not os.path.exists(ele_img_dir_path):
            os.mkdir(ele_img_dir_path)
        # 构建元素截图保存路径
        ele_img_path = ele_img_dir_path + ele_name
        self.element_get(driver, locate_type, locator_expression).screenshot(ele_img_path)
        return ele_img_path

    def scroll_to_element(self, driver, locate_type, locator_expression):
        """
        滚动到元素
        :param driver:
        :param locate_type:
        :param locator_expression:
        :return:
        """
        ele = self.element_get(driver, locate_type, locator_expression)
        driver.execute_script("arguments[0].scrollIntoView()", ele)
        return True

